从炒菜到编程:揭秘Java/Go/Rust异步背后的哲学

要想开发出一款高效的程序,异步编程是必不可少的,而且Java、Go Rust 语言都通过不同方式支持异步编程。

何为异步

那么何为异步编程呢?

与异步相对的概念是同步,同步是指做完一件事再继续做另一件事,依次按照顺序完成每一件事。而异步恰恰相反,多件事情可以并行做,或者交替执行。举一个相对生活化的例子,来比较形象的说明下同步和异步的区别。

比如今天晚饭想给自己加个菜,于是开始按照菜谱进行炒菜,首先向锅中倒入食用油,等油热之后再放入相应的食材和调味品,然后进行翻炒,最后放入盘中进行享用。
现在可以吃饭了,现在可以一口米饭一口菜的干饭了,不过这时候还想看个视频或者听个音乐,一边干饭一边娱乐下。

这个例子中炒菜的过程就是同步的,必须把某一步做完之后再进行下一步。而吃饭的过程就是异步的,吃米饭和菜可以交替执行,吃饭和娱乐可以并行执行。下面用一个图来更形象的感受下同步和异步。

接下来我们看下各个编程语言是如何进行异步编程的,这里主要看下Java、Golang和Rust的异步编程。

Java异步编程

Java是通过多线程来实现异步编程的,主线程会为每个任务新建一个任务线程,各个任务线程之间无需相互等待,而是并行执行,相互之间没有影响,所有任务线程都执行结束之后主线程运行结束。

public class AsyncDiner {
    public static void main(String[] args) {  

        Thread eatRiceThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("吃口米饭");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread eatDishThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("吃口菜");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread entertainThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("娱乐中。。。");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 启动三个线程
        eatRiceThread.start();  
        eatDishThread.start();
        entertainThread.start();

        // 等待所有线程完成
        try {
            eatRiceThread.join();  
            eatDishThread.join();
            entertainThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("干饭+娱乐全部完成!");
    }
}Code language: JavaScript (javascript)

但是随着Java不断的发展,目前已支持多种方式来实现异步编程,比如CompletableFuture,还可以通过第三方来实现更高效的异步编程。

Golang异步编程

Golang是通过协程来实现异步编程的。Golang的协程是其语言的一大特性,其使用简单,只需在需要异步执行的地方加上go关键字即可,而且性能也很高。

package main

import (
    "fmt"
    "sync"
    "time"
)

func eatRice(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 3; i++ {
        fmt.Println("吃口米饭")
        time.Sleep(1 * time.Second)
    }
}

func eatDish(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 3; i++ {
        fmt.Println("吃口菜")
        time.Sleep(1 * time.Second)
    }
}

func entertain(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 3; i++ {
        fmt.Println("娱乐中。。。")
        time.Sleep(1 * time.Second)
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(3) // 启动三个并发任务

    go eatRice(&wg)
    go eatDish(&wg)
    go entertain(&wg)

    wg.Wait() // 等待所有 goroutine 结束
    fmt.Println("干饭+娱乐全部完成!")
}Code language: JavaScript (javascript)

Rust异步编程

Rust也是通过协程来实现异步编程的,通过async/await 语法实现,与Golang不同的是,Rust并没有自带异步运行时,而是靠社区提供,这里的例子使用tokio提供异步运行时。

由于使用了第三方库,所以需要在项目的Cargo.toml中添加相应的依赖,如下:

# [dependencies]
tokio = { version = "1.45.0", features = ["full"] }Code language: PHP (php)

例子代码如下:

use tokio::time::{sleep, Duration};

async fn eat_rice() {
    for i in 1..=3 {
        println!("吃口米饭");
        sleep(Duration::from_secs(1)).await;
    }
}

async fn eat_dish() {
    for i in 1..=3 {
        println!("吃口菜");
        sleep(Duration::from_secs(1)).await;
    }
}

async fn entertain() {
    for i in 1..=3 {
        println!("娱乐中。。。");
        sleep(Duration::from_secs(1)).await;
    }
}

#[tokio::main]
async fn main() {
    // 并发执行三个异步任务
    let ((), (), ()) = tokio::join!(
        eat_rice(),
        eat_dish(),
        entertain(),
    );

    println!("干饭+娱乐全部完成!");
}Code language: PHP (php)

线程和协程

上文中提到两个概念,一个是线程,另一个是协程

  • 线程是操作系统中调度的最小单元,是进程的一个执行路径,由操作系统进行调度,是抢占式的。线程之间通过共享内存来通信。
  • 协程是用户态的轻量级线程,由程序自己控制,不受操作系统调度,是协作式的。协程之间通过Channel进行通信。

线程是由CPU执行,在单核CPU上,多个线程轮流执行,这时线程的切换需要交换上下文,开销较大,在多核CPU上,多个线程可以并行执行,可以并行的线程数与核数相同,线程再多就需要排队等待CPU调度了。而协程是在线程中执行,单个线程中可并行多个协程,所以协程之间的切换不涉及上下文的交换,开销较小。

总结

这里通过炒菜和吃饭的例子介绍了什么是同步和异步,同时通过代码示例介绍了Java、Golang和Rust的异步实现,其中Java通过线程实现异步,而Golang和Rust则是通过协程来实现异步,其线程没有协程的性能高,但也并不是协程就一定比线程好,只是各自适合的场景不一样,其中线程比较适合CPU密集型的大任务,可以多核并行执行,比如一些大数据计算场景,而协程比较适合IO密集型的轻量任务,可以高并发快速执行,比如一些http请求。

这篇文章主要介绍下何为异步编程和各个编程语言都是如何实现的,在接下来的文章中会逐步介绍下Rust中async的原理和async在一些具体的场景中如何发挥其巨大的作用的。